eletrotupi / tcc/ frontend/app/entry/[id].tsx22ce11e7f70ff2faf4796eef6498cd1ec65bbd31
13.5 KB | 444 lines Raw
1
import { useState, useEffect, use } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
16
import { Badge } from "@/components/ui/Badge";
17
import { Button } from "@/components/ui/Button";
18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19
import { Section, SectionHeader } from "@/components/ui/Sections";
20
import { Theme, Colors, Spacing } from "@/constants/theme";
21
import { getMood } from "@/constants/moods";
22
import {
23
  getMoodComponent,
24
  intensityLabel,
25
  intensityToValue,
26
} from "@/constants/mood-components";
27
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
28
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
29
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
30
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
31
import { getTriggerCategory } from "@/constants/trigger-categories";
32
import { getCareActionMeta } from "@/constants/care-action-categories";
33

34
const impactColor = (n: number) => {
35
  if (n <= 1) return '#86efac';
36
  if (n <= 2) return '#22c55e';
37
  if (n <= 3) return '#f59e0b';
38
  if (n <= 4) return '#f97316';
39
  return '#ef4444';
40
};
41

42
export default function MoodDetailScreen() {
43
  const { id } = useLocalSearchParams<{ id: string }>();
44
  const { data: entry, isLoading, isError } = useMoodEntry(id);
45
  const deleteMoodEntry = useDeleteMoodEntry();
46
  const [minutesLeft, setMinutesLeft] = useState(0);
47
  const [canDelete, setCanDelete] = useState(false);
48

49
  useEffect(() => {
50
    const updateTimer = () => {
51
      const moment = new Date(entry?.moment);
52
      const createdAt = new Date(entry?.moment);
53
      const msSinceCreation = Date.now() - createdAt.getTime();
54
      const deletionWindowMs = 5 * 60 * 1000;
55

56
      const newCanDelete = msSinceCreation < deletionWindowMs;
57
      const newMinutesLeft = Math.max(
58
        0,
59
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
60
      );
61

62
      setCanDelete(newCanDelete);
63
      setMinutesLeft(newMinutesLeft);
64
    };
65

66
    updateTimer(); // Set initial values
67

68
    const interval = setInterval(updateTimer, 1000);
69

70
    return () => clearInterval(interval);
71
  }, [entry?.moment]);
72

73
  if (isLoading) {
74
    return (
75
      <View style={styles.centered}>
76
        <ActivityIndicator size="large" color={Theme.colors.tint} />
77
      </View>
78
    );
79
  }
80

81
  if (isError || !entry) {
82
    return (
83
      <View style={styles.centered}>
84
        <Text style={styles.errorText}>Registro não encontrado.</Text>
85
        <Pressable onPress={() => router.back()} style={styles.backButton}>
86
          <Text style={styles.backButtonText}>Voltar</Text>
87
        </Pressable>
88
      </View>
89
    );
90
  }
91

92
  const moment = new Date(entry.moment);
93
  const moodDef = getMood(entry.selectedMood.toLowerCase());
94
  const badge = getTimeBadge(moment);
95

96
  const handleDelete = async () => {
97
    await deleteMoodEntry.mutateAsync(String(entry.id));
98

99
    router.back();
100
  };
101

102
  const deleteButtonMsg = () => {
103
    const msg = "Excluir Registro";
104
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
105
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
106

107
    return `${msg} ${suffix}`;
108
  };
109

110
  const hasAnyLinks = (
111
    entry.triggerLinks && entry.triggerLinks.length > 0
112
  ) || (entry.careActions && entry.careActions.length > 0);
113

114
  return (
115
    <>
116
      <Stack.Screen options={{ title: "Ver detalhes" }} />
117

118
      <ScrollView
119
        style={styles.container}
120
        contentContainerStyle={styles.content}
121
        showsVerticalScrollIndicator={false}
122
      >
123
        {/* Header card */}
124
        <Card style={styles.headerCard}>
125
          <Between>
126
            <Row gap={12} style={{ alignItems: "center" }}>
127
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
128
              <Col gap={2}>
129
                <Text style={styles.moodLabel}>
130
                  {moodDef?.label ?? entry.selectedMood}
131
                </Text>
132
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
133
              </Col>
134
            </Row>
135
            {/* If this is the latest mood entry, show the active badge */}
136
            <Badge label="Registro Ativo" variant="green" />
137
          </Between>
138
        </Card>
139

140
        {/* Core levels */}
141
        <Section>
142
          <SectionHeader title="Níveis emocionais" variant="subtle" />
143
          {[
144
            { id: "energy", label: "Energia", value: entry.energyLevel },
145
            { id: "stress", label: "Estresse", value: entry.stressLevel },
146
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
147
          ].map((item, index, arr) => (
148
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
149
              <View key={item.label}>
150
                <View style={styles.componentRow}>
151
                  <Text style={styles.componentLabel}>{item.label}</Text>
152
                  <Text style={styles.componentIntensityGreen}>
153
                    {item.value}/10
154
                  </Text>
155
                </View>
156

157
                <View style={styles.progressTrack}>
158
                  <View
159
                    style={[
160
                      styles.progressFill,
161
                      { width: `${(item.value / 10) * 100}%` },
162
                    ]}
163
                  />
164
                </View>
165
              </View>
166
            </Card>
167
          ))}
168
        </Section>
169

170
        <Section>
171
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
172
          <Col gap={0}>
173
            {entry.moodComponents.map((comp, index) => {
174
              const def = getMoodComponent(comp.component.toLowerCase());
175
              const numericIntensity =
176
                comp.intensity === "LIGHT"
177
                  ? 2
178
                  : comp.intensity === "MODERATE"
179
                    ? 5
180
                    : 8;
181

182
              return (
183
                <MoodComponentCard
184
                  key={comp.id}
185
                  id={comp.component.toLowerCase()}
186
                  intensity={comp.intensity}
187
                />
188
              );
189
            })}
190
          </Col>
191
        </Section>
192

193
        {/* Linked triggers */}
194
        <Section>
195
          <SectionHeader title="Vínculo" variant="subtle" />
196
          {!!hasAnyLinks ? (
197
            <Col gap={8}>
198
              {entry.triggerLinks.map((link) => {
199
                const cat = getTriggerCategory(link.trigger.category);
200
                return (
201
                  <Pressable
202
                    key={link.id}
203
                    onPress={() => router.push(`/triggers/${link.trigger.id}`)}
204
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
205
                  >
206
                    <Card style={styles.linkCard}>
207
                      <Row style={{ alignItems: "center", gap: 12 }}>
208
                        <View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
209
                          <MaterialIcons name={cat.icon} size={22} color={cat.color} />
210
                        </View>
211
                        <Col gap={3} style={{ flex: 1 }}>
212
                          <Row style={{ alignItems: "center", gap: 6 }}>
213
                            <Text style={styles.linkLabel}>{cat.label}</Text>
214
                            <Badge label="Gatilho" variant="orange" style={undefined} />
215
                          </Row>
216
                          <Text style={styles.linkTime}>
217
                            {formatMoment(new Date(link.trigger.moment))}
218
                          </Text>
219
                        </Col>
220
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
221
                      </Row>
222
                      <View style={styles.impactBar}>
223
                        <View
224
                          style={[
225
                            styles.impactFill,
226
                            {
227
                              width: `${(link.perceivedImpact / 5) * 100}%`,
228
                              backgroundColor: impactColor(link.perceivedImpact),
229
                            },
230
                          ]}
231
                        />
232
                      </View>
233
                    </Card>
234
                  </Pressable>
235
                );
236
              })}
237

238
              {(entry.careActions ?? []).map((ca) => {
239
                const meta = getCareActionMeta(ca);
240

241
                return (
242
                  <Pressable
243
                    key={ca.id}
244
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
245
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
246
                  >
247
                    <Card style={styles.linkCard}>
248
                      <Row style={{ alignItems: 'center', gap: 12 }}>
249
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
250
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
251
                        </View>
252
                        <Col gap={3} style={{ flex: 1 }}>
253
                          <Row style={{ alignItems: 'center', gap: 6 }}>
254
                            <Text style={styles.linkLabel}>{meta.label}</Text>
255
                            <Badge label="Consulta" variant="blue" style={undefined} />
256
                          </Row>
257
                          <Text style={styles.linkTime}>
258
                            {formatMoment(new Date(ca.moment))}
259
                          </Text>
260
                        </Col>
261

262
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
263
                      </Row>
264
                    </Card>
265
                  </Pressable>
266
                );
267
              })}
268
            </Col>
269
          ) : (
270
            <NoLinkMessage />
271
          )}
272
        </Section>
273

274
        {/* Annotation / notes */}
275
        {entry.annotation && (
276
          <Section>
277
            <SectionHeader title="Anotações" variant="subtle" />
278
            <Card style={{ padding: Spacing.cardGap }}>
279
              <Text style={styles.annotation}>
280
                "{entry.annotation?.trim()}"
281
              </Text>
282
            </Card>
283
          </Section>
284
        )}
285

286
        {/* Delete */}
287
        <View style={styles.section}>
288
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
289
          <Button
290
            variant="danger"
291
            loading={deleteMoodEntry.isPending}
292
            disabled={!canDelete || deleteMoodEntry.isPending}
293
            onPress={handleDelete}
294
            title={deleteButtonMsg()}
295
          />
296

297
          <SubtleInfoCard
298
            style={styles.deleteInfo}
299
            text={`
300
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
301
              após o envio para garantir a integridade do histórico emocional.
302
              `}
303
          />
304
        </View>
305
      </ScrollView>
306
    </>
307
  );
308
}
309

310
const styles = StyleSheet.create({
311
  container: {
312
    flex: 1,
313
    backgroundColor: Theme.colors.background,
314
  },
315
  content: {
316
    padding: Spacing.containerPadding,
317
    gap: Spacing.sectionGap,
318
    paddingBottom: 48,
319
  },
320
  centered: {
321
    flex: 1,
322
    justifyContent: "center",
323
    alignItems: "center",
324
    gap: 12,
325
  },
326
  errorText: {
327
    fontSize: 15,
328
    color: Colors.light.textSecondary,
329
  },
330
  backButton: {
331
    paddingHorizontal: 20,
332
    paddingVertical: 10,
333
    backgroundColor: Theme.colors.tint,
334
    borderRadius: Theme.borderRadius.md,
335
  },
336
  backButtonText: {
337
    fontWeight: "700",
338
    color: Theme.colors.text,
339
  },
340

341
  // Header
342
  headerCard: {
343
    padding: 16,
344
  },
345
  moodIcon: {
346
    fontSize: 40,
347
  },
348
  moodLabel: {
349
    fontSize: 17,
350
    fontWeight: "700",
351
    color: Colors.light.text,
352
  },
353
  moodTime: {
354
    fontSize: 13,
355
    color: Colors.light.textSecondary,
356
  },
357

358
  // Sections
359
  section: {
360
    gap: 8,
361
  },
362
  sectionLabel: {
363
    fontSize: 11,
364
    fontWeight: "600",
365
    letterSpacing: 0.8,
366
    color: Colors.light.textSecondary,
367
    textTransform: "uppercase",
368
  },
369

370
  // Components
371
  componentRow: {
372
    flexDirection: "row",
373
    justifyContent: "space-between",
374
    alignItems: "center",
375
    paddingVertical: 10,
376
  },
377
  componentLabel: {
378
    fontSize: 15,
379
    fontWeight: "700",
380
    color: Colors.light.text,
381
  },
382
  componentIntensity: {
383
    fontSize: 13,
384
    color: Colors.light.textSecondary,
385
  },
386
  componentIntensityGreen: {
387
    fontSize: 13,
388
    fontWeight: "700",
389
    color: Theme.colors.tint,
390
  },
391
  progressTrack: {
392
    height: 6,
393
    backgroundColor: Theme.colors.light.divider,
394
    borderRadius: Theme.borderRadius.full,
395
    marginBottom: 4,
396
    overflow: "hidden",
397
  },
398
  progressFill: {
399
    height: "100%",
400
    backgroundColor: Theme.colors.light.tint,
401
    borderRadius: Theme.borderRadius.full,
402
  },
403
  // Annotation
404
  annotation: {
405
    fontSize: 14,
406
    lineHeight: 22,
407
    color: Colors.light.textSecondary,
408
    fontStyle: "italic",
409
    padding: 4,
410
  },
411
  // Linked trigger cards
412
  linkCard: {
413
    padding: Spacing.cardGap,
414
    overflow: "hidden",
415
    gap: 10,
416
  },
417
  linkIconCircle: {
418
    width: 44,
419
    height: 44,
420
    borderRadius: 22,
421
    alignItems: "center",
422
    justifyContent: "center",
423
  },
424
  linkTime: {
425
    fontSize: 12,
426
    color: Colors.light.textSecondary,
427
  },
428
  linkLabel: {
429
    fontSize: 15,
430
    fontWeight: "700",
431
    color: Colors.light.text,
432
  },
433
  impactBar: {
434
    height: 3,
435
    backgroundColor: Colors.light.divider,
436
    borderRadius: 99,
437
    overflow: "hidden",
438
  },
439
  impactFill: {
440
    height: "100%",
441
    borderRadius: 99,
442
  },
443

444
});